package com.mapforge.gui.map;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import com.mapforge.gui.interfaces.TileSelectionGUIInterface;
import com.mapforge.listener.TileMapCreationEvent;
import com.mapforge.listener.TileSelectionEvent;
import com.mapforge.listener.TileSelectionListener;
import com.mapforge.listener.TilesetEditEvent;
import com.mapforge.map.Tile;
import com.mapforge.map.Tileset;
import com.mapforge.map.tools.undo.TilePoint;
import com.mapforge.selection.ComparablePoint;
import com.mapforge.selection.Selection;
import com.mapforge.selection.TileSelection;

/**
 * A panel which allows displays a {@link Tileset}, and allows a user make
 * {@link Selection selections} of {@link Tile tiles} from that {@link Tileset
 * Tileset.}
 */
public class TilesetSelectionPanel extends TilesetPanel implements
		TileSelectionGUIInterface {
	/**
	 * Default serial version ID, here to remove warnings.
	 */
	private static final long serialVersionUID = 1L;

	/**
	 * Currently selected tiles
	 */
	private TileSelection selectedTiles;

	/**
	 * Where a drag action ends
	 */
	private Point dragEnd;

	/**
	 * Where a drag action starts
	 */
	private Point dragStart;

	/**
	 * Bounding rectangle for the selected tiles
	 */
	private Rectangle dragRect;

	private boolean controlDown;

	/**
	 * List of listeners interested in tile selection events
	 */
	private List<TileSelectionListener> tileSelectionListeners;

	public TilesetSelectionPanel() {
		this(null, null);
	}

	public TilesetSelectionPanel(Tileset tileset, BufferedImage img) {
		super(tileset, img);
		this.tileSelectionListeners = new ArrayList<TileSelectionListener>();
		// this.compositeSelection = new CheckedCompositeSelection();
		this.dragRect = new Rectangle();
		this.addMouseListener(this);
		this.addMouseMotionListener(this);
		this.setFocusable(true);
		this.controlDown = false;
	}

	/**
	 * Registers a listener to be notified each time there's a tile selection
	 * event.
	 * 
	 * @param listener
	 *            listener to be registered
	 */
	public void addTileSelectionListener(TileSelectionListener listener) {
		tileSelectionListeners.add(listener);
	}

	protected void fireTileSelectionPerformed(TileSelectionEvent event) {
		for (int i = 0; i < tileSelectionListeners.size(); i++) {
			tileSelectionListeners.get(i).tileSelectionPerformed(event);
		}
	}

	/**
	 * Notifies all registered listeners that a tile selection has occurred.
	 * 
	 * @param tileSelectionEvent
	 *            the event which has occurred
	 */
	protected void fireTileSelectionPerformed() {
		// TODO make a wrapper or something for selection, right now users could
		// alter selection without informing registered listeners
		TileSelectionEvent tileSelectionEvent = new TileSelectionEvent(this,
				selectedTiles);
		fireTileSelectionPerformed(tileSelectionEvent);
	}

	/**
	 * 
	 * @return
	 */
	public Selection getTilesSelected() {
		return selectedTiles;
	}

	private Point pointAt(Point p) {
		if (getTileset() != null) {
			Tileset tileset = getTileset();
			int imgw = tileset.getColumns();
			int imgh = tileset.getRows();
			Point point = new Point(p.x / tileset.getTileWidth(), p.y
					/ tileset.getTileWidth());
			point.x = point.x < imgw ? point.x : imgw - 1;
			point.y = point.y < imgh ? point.y : imgh - 1;
			point.x = point.x < 0 ? 0 : point.x;
			point.y = point.y < 0 ? 0 : point.y;
			return point;
		} else
			return null;
	}

	private void setDragRect(Point p1, Point p2) {
		int x = Math.min(p2.x, p1.x);
		int y = Math.min(p2.y, p1.y);
		int w = Math.abs(p2.x - p1.x) + 1;
		int h = Math.abs(p2.y - p1.y) + 1;
		dragRect.setFrame(x, y, w, h);
	}

	private void handleTileSelection(TileSelection selection) {
		Iterator<ComparablePoint> it;
		ComparablePoint p;
		// control is down
		if (controlDown) {
			it = selection.iterator();
			boolean contains = true;
			// check if old selection fully contains this selection
			while (it.hasNext() && contains) {
				p = it.next();
				if (!selectedTiles.contains(p.x, p.y)) {
					contains = false;
				}
			}
			Tileset tileset = this.getTileset();
			if (contains) {
				this.selectedTiles.subtract(selection);
			} else {
				it = selection.iterator();
				while (it.hasNext()) {
					p = it.next();
					this.selectedTiles.addPoint(new TilePoint(tileset
							.getTileAt(p.x, p.y), p.x, p.y));
				}
			}
		}
		// just replace previous selection
		else {
			this.selectedTiles = selection;
		}
	}

	public Rectangle getSelectedTilesBoundingRectangle() {
		return (Rectangle) dragRect.clone();
	}

	public void mouseEntered(MouseEvent e) {
		// intentionally left blank
	}

	public void mouseExited(MouseEvent arg0) {
		// intentionally left blank
	}

	public void mousePressed(MouseEvent e) {
		if (dragStart == null) {
			Point p = pointAt(e.getPoint());
			if (p != null) {
				dragEnd = dragStart = p;
				dragRect.setFrame(dragStart.x, dragStart.y, 1, 1);
				repaint();
			}
		}
	}

	public void mouseReleased(MouseEvent e) {
		TileSelection selection = new TileSelection();
		if (dragRect.width == 1 && dragRect.height == 1) {
			selection.addPoint(new TilePoint(this.getTileset().getTileAt(
					dragRect.x, dragRect.y), dragRect.x, dragRect.y));
		} else {
			Tileset tileset = this.getTileset();
			int right = dragRect.x + dragRect.width;
			int bottom = dragRect.y + dragRect.height;
			for (int y = dragRect.y; y < bottom; y++) {
				for (int x = dragRect.x; x < right; x++) {
					selection.addPoint(new TilePoint(tileset.getTileAt(x, y),
							x, y));
				}
			}
			// selection = new ContinuousSelection((Rectangle)
			// dragRect.clone());
		}
		handleTileSelection(selection);
		this.fireTileSelectionPerformed();
		this.dragStart = null;
		repaint();
	}

	@Override
	public void mouseDragged(MouseEvent e) {
		Point p = pointAt(e.getPoint());
		if (p != null) {
			if (p.x != dragEnd.x || p.y != dragEnd.y) {
				dragEnd = p;
				setDragRect(dragStart, dragEnd);
				repaint();
			}
		}
	}

	public void mouseMoved(MouseEvent e) {
		// intentionally left blank
	}

	protected void paintSelectedTilesBorder(Graphics g) {
		Tileset tileset = getTileset();
		Iterator<ComparablePoint> it = this.selectedTiles.iterator();
		int tW = tileset.getTileWidth();
		int tH = tileset.getTileHeight();
		ComparablePoint p;
		while (it.hasNext()) {
			p = it.next();
			int x = p.x * tW;
			int y = p.y * tH;
			g.setColor(Color.BLACK);
			g.drawRect(x, y, tW, tH);
			g.setColor(Color.WHITE);
			g.drawRect(x + 1, y + 1, tW - 2, tH - 2);
			g.drawRect(x - 1, y - 1, tW + 2, tH + 2);
		}
		if (tileset != null && dragStart != null) {
			int w = dragRect.width * tW;
			int h = dragRect.height * tH;
			int x = dragRect.x * tW;
			int y = dragRect.y * tH;
			g.setColor(Color.BLACK);
			g.drawRect(x, y, w, h);
			g.setColor(Color.BLUE);
			g.drawRect(x + 1, y + 1, w - 2, h - 2);
			g.drawRect(x - 1, y - 1, w + 2, h + 2);
		}
		// draws bounding rectangle
		/*
		 * Rectangle rect = selectedTiles.getBoundingRectangle(); int w =
		 * rect.width * tW; int h = rect.height * tH; int x = rect.x * tW; int y
		 * = rect.y * tH; g.setColor(Color.BLACK); g.drawRect(x, y, w, h);
		 * g.setColor(Color.RED); g.drawRect(x + 1, y + 1, w - 2, h - 2);
		 * g.drawRect(x - 1, y - 1, w + 2, h + 2);
		 */

	}

	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		paintSelectedTilesBorder(g);
	}

	/**
	 * If some other component performs a selection event, it will invalidate
	 * this panel's selection.
	 */
	public void tileSelectionPerformed(TileSelectionEvent tileSelectionEvent) {
		dragRect.setFrame(0, 0, 0, 0);
		this.selectedTiles.clearSelection();
		repaint();
	}

	@Override
	public void tileMapCreationPerformed(TileMapCreationEvent mapCreationEvent) {
		Tileset tileset = mapCreationEvent.getTileMapCreated().getTileset();
		int emptyIndex = tileset.getEmptyTileIndex();
		int x = emptyIndex / tileset.getColumns();
		int y = emptyIndex / tileset.getRows();
		this.selectedTiles = new TileSelection();
		this.selectedTiles
				.addPoint(new TilePoint(tileset.getTileAt(x, y), x, y));
		this.dragRect.setFrame(x, y, 1, 1);
		this.setTileset(tileset, mapCreationEvent.getTilesetImage());
		this.fireTileSelectionPerformed();
	}

	@Override
	public void tilesetEditHappened(TilesetEditEvent evt) {
		// TODO Auto-generated method stub
	}

	@Override
	public boolean removeTileSelectionListener(TileSelectionListener listener) {
		return tileSelectionListeners.remove(listener);
	}

	@Override
	public void clearTileSelectionListeners() {
		this.tileSelectionListeners.clear();
	}

	@Override
	public void keyPressed(KeyEvent e) {
		if (e.getKeyCode() == KeyEvent.VK_CONTROL) {
			controlDown = true;
		}
	}

	@Override
	public void keyReleased(KeyEvent e) {
		if (e.getKeyCode() == KeyEvent.VK_CONTROL) {
			controlDown = false;
		}
	}

	@Override
	public void keyTyped(KeyEvent e) {
		// intentionally left blank
	}

	@Override
	public void mouseClicked(MouseEvent arg0) {
		// TODO Auto-generated method stub

	}
}
